/**
 * RHRM Frame 1.0.6
 *      Vinlic      
 */

/**
 *
 * 框架模块引入
 *
 * koa    koa2框架本体
 * router    路由模块(特制)
 * logger    控制台显示请求信息模块
 * bodyParser    请求体接收转换模块
 * xmlParser    XML数据接收转换模块
 * cors    跨域请求模块
 * multer    文件上传控制模块
 * fs    文件操作模块
 * argv    控制台参数接收模块
 * Redis    Redis数据库操作封装模块
 * MySQL    MySQL数据库操作封装模块
 * common    框架工具封装类模块
 * config    框架配置文件
 *
 */

"use strict"
const koa = require('koa')
const router = require('./libs/koa-router')()
const logger = require('koa-logger')
const bodyParser  = require('koa-bodyparser')
const xmlParser = require('koa-xml-body')
const cors = require('koa2-cors')
const multer = require('koa-multer')
const fs = require('fs-extra')
const argv = require('optimist').argv
const Redis = require('./redis')
const MySQL = require('./mysql')
const Common = require('./common')
const Printer = require('./printer')
const config = require('./config')

/**
 *
 * 框架初始化
 *
 */

//获得框架实例
const app = new koa()

//如果控制台传入了p参数则采用该参数为端口号否则使用默认端口
const serverPort = argv.p || config.server.port

//定时任务池
let cronPool = {}

//mysql连接池
let mysqlPool = {}

//redis连接池
let redisPool = {}

//实例化工具类
const common = new Common({ config, fs, cronPool })

/**
 *
 * 线程级信号监听处理
 *
 * exit 监听线程结束退出
 *
 */

process.on('exit', (code) => {
	common.borderErr(`server is stopped(${code})`)
	//服务器异常退出写入log
	common.writeLog('error', `server was terminated(code:${code})`)
})

/**
 *
 * 框架启动
 *
 */

//写入服务器启动log
common.writeLog('info', 'server starting...')

//连接所有需要连接的MySQL数据库
config.mysql.dbs.forEach((dbName) => {
	let {dbPrefix, host, port, user, pwd, charset} = config.mysql 
	mysqlPool[(dbName.indexOf(dbPrefix) == -1 ? dbName : dbName.replace(`${dbPrefix}_`, ''))] = new MySQL(common, {host, port, user, pwd, name: dbName, charset})
})

//连接所有需要连接的Redis数据库
config.redis.dbs.forEach((dbName, dbIndex) => {
	let {host, port, pwd} = config.redis
	redisPool[dbName] = new Redis(common, {host, port, pwd, db: dbIndex, name: dbName})
})

//让工具类具有操作redis功能
common.redis = redisPool

//实例化打印类
const printer = new Printer({common, config: config.printer, redis: redisPool})

//初始化文件上传接收器
const uploader = multer({
	storage: multer.diskStorage({  
		//文件临时保存路径  
		destination: (req, file, cb) => {
			cb(null, config.server.tmpPath)  
		},  
		//修改文件名称  
		filename: (req, file, cb) => {
			let tmpId = parseInt(Date.now())
			let fileFormat = (file.originalname).split(".") 
			cb(null, tmpId.toString())
		}
	}),
	fileFilter: (req, data, cb) => {
		let {names, mimes} = config.fileFilter
		for(let f of names) {
			if(f == data.fieldname) {
				for(let m of mimes) {
					if(m == data.mimetype) {
						cb(null, true)
						return
					}
				}
				cb(null, false)
			}
		}
	},
	limits: {
		fileSize: config.fileFilter.maxSize
	}
})

//注册中间件到框架
app.use(logger())
app.use(xmlParser({
	xmlOptions: {
		explicitArray: false
	},
	onerror: (err, ctx) => {
		console.error(err)
		common.writeLog('error', `parse xml data failed:${err.status} ${err.message}`)
		ctx.body = parseError('-2056', 'parse post xml data failed')
	}
})).use((ctx,next) => {
	console.log(ctx.request.body)
	ctx.xmlData = (ctx.request.body && ctx.request.body.xml ? ctx.request.body.xml : ctx.request.body)
	return next()
})
app.use(bodyParser())
app.use(cors())

//输出框架显示
common.borderMsg(`RHRM Frame ${config.frame.version}`, 'Vinlic')

const parseData = (data = {}) => {
	return JSON.stringify({
		status: 'success',
		code: '0',
		data,
	})
}

const parseError = (code, msg, notPrint = false) => {
	if(!notPrint)
		console.error(code,msg)
	if(Object.prototype.toString.call(msg) === '[object String]') {
		return `{"status":"failed","code":"${code}","msg":"${msg}"}`
	}
	return JSON.stringify({
		status: 'failed',
		code,
		msg
	})
}

const resolve = (data = 0) => {
	return Promise.resolve(data)
}

const reject =  (code, msg = 'handle failed') => {
	return Promise.reject({code, msg})
}

const checkSign = async (uri, ...datas) => {
	try {
		let data = {}
		let sessionData = {}
		for(let _data of datas)
			Object.assign(data, _data)
		if(!data || !data.grantType || ['token', 'key'].indexOf(data.grantType) == -1 || !data.sign || data.sign.length != 32)
			return reject('-1025', 'sign error')
		//API失效时间，如果没定义将为5分钟
		let apiExpires = config.frame.apiExpires[uri] || config.frame.apiExpires.all || 300
		if(!data.timestamp || !data.nonce || isNaN(data.timestamp) || data.nonce.length != 10 || (common.timestamp() - Math.abs(parseInt(data.timestamp))) > apiExpires)
			return reject('-1030', 'sign timeout')
		let sign = ''
		let signStr = ''
		let keys = Object.keys(data)
		if(keys.length == 1) {
			return reject('-1026', 'sign error')
		}
		keys = keys.sort()
		for(let key of keys) {
			if(key == 'sign') {
				sign = data[key]
			}
			else if(!isNaN(key)) {
				continue
			}
			else if(Object.prototype.toString.call(data[key]) == '[object Object]' || Object.prototype.toString.call(data[key]) == '[object Array]') {
				signStr += `&${key}=${JSON.stringify(data[key])}`
			}
			else {
				signStr += `&${key}=${encodeURI(data[key])}`
			}
		}
		switch(data.grantType) {
			case 'token':
				if(!data.session || !/([a-f\d]{8}(-[a-f\d]{4}){3}-[a-f\d]{12}?)/i.test(data.session))
					return reject('-1004', `request interface permission denied`)
				let user = await redisPool.session.get(data.session, true)
				console.log(user)
				if(!user || !user.type || !user.token || user.token.length != 10 || !user.data) {
					return reject('-1004', `request interface permission denied`)
				}
				signStr += `&token=${user.token}`
				user.session = data.session
				sessionData = user
			break
			case 'key':
				if(data.appId) {
					return reject('-1035', 'sign appid invalid')
				}
				signStr += `&key=${config.frame.apiKeys[data.appId]}`
			break
		}
		signStr = common.ltrim(signStr, '&')
		console.log(signStr)
		signStr = common.md5(signStr)
		console.log(signStr)
		if(sign == signStr) {
			if(config.frame.apiStrict) {
				let nonce = await redisPool.apiNonce.get(data.nonce)
				if(nonce)
					return reject('-1043', 'sign error')
				else
					await redisPool.apiNonce.set(data.nonce, {session: data.session}, apiExpires)
			}
			console.log(`${data.grantType} sign success`)
			return resolve(sessionData)
		}
		else {
			console.error('sign error')
			return reject('-1028', 'sign error')
		}
	}
	catch(err) {
		console.error(err)
		return reject('-1027', 'sign error')
	}
}

/**
 *
 * 注册处理件
 *
 * 用于供路由件调用的处理库
 *
 */

//处理件对象
let handles = {}
config.frame.handles.forEach((handleName) => {
	try {
		const Handle = require(`./handles/h_${handleName}`)
		handles[handleName] = new Handle()
		Object.assign(handles[handleName], {common, config, fs, uploader, mysql: mysqlPool, redis: redisPool, cron: cronPool, printer,
			resolve,
			reject
		})
		console.log(`handle ${handleName} loaded`)
	}
	catch(err) {
		console.error(err)
		console.error(`handle ${handleName} load failed`)
		//写入注册处理件失败log
		common.writeLog('error', `handle ${handleName} load failed`)
	}
})

const routerExt = {
	echo: (data) => {
		return Promise.resolve({echo:  true, data})
	},
	data: (data, notPrint = true) => {
		if(!notPrint)
			console.log(data)
		return Promise.resolve({code: '0', data})
	},
	error: (code, msg) => {
		console.error(code, msg)
		return Promise.reject({code, msg})
	},
	allow: async function(...userTypes) {
		const user = this.user
		if(!user || !user.type || userTypes.indexOf(user.type) == -1) {
			this.status = 403
			return Promise.reject({
				code: '-1038',
				msg: 'user cannot request this interface'
			})
		}
		return Promise.resolve(user)
	}
}

/**
 *
 * 注册路由件
 *
 * 处理请求的路由件
 *
 */

config.frame.routers.forEach((routerName) => {
	try {
		const _router = require(`./routers/r_${routerName}`)
		for(let method in _router) {
			if(['all', 'get', 'post', 'put', 'delete'].indexOf(method) == -1) {
				console.error(`${method} is invalid method`)
				continue
			}
			const pathArr = Object.keys(_router[method])
			if(pathArr.length == 0)
				continue
			router[method](pathArr,  async (ctx, next) => {
				if(!ctx.index && ctx.index != 0)
					ctx.index = 0
				const path = ctx.matched[ctx.index].path
				const _method = ctx.request.method.toLowerCase()
				const uri = path.substring(config.frame.uriPrefix.length)
				if((!_router[_method] || !_router[_method][uri] || Object.prototype.toString.call(_router[_method][uri])!= '[object AsyncFunction]') && (!_router['all'] || !_router['all'][uri] || Object.prototype.toString.call(_router[_method][uri]) != '[object AsyncFunction]')) {
					ctx.index++
					await next()
					return
				}
				ctx.originIP = ctx.headers['x-real-ip'] || ctx.headers['x-forwarded-for'] || (ctx.ip ? ctx.ip.substr(7, ctx.ip.length - 1) : '0.0.0.0')
				ctx.uri = uri
				ctx.request.method = _method
				ctx.getData = ctx.query
				ctx.postData = ctx.request.body
				Object.assign(ctx, {handles, common, config, fs, uploader, mysql: mysqlPool, redis: redisPool, printer}, routerExt)
				try {
					let target
					console.log(ctx.uri)
					for(let m in config.frame.noSign) {
						if((_method == 'all' || ctx.request.method == m) && config.frame.noSign[m].indexOf(ctx.uri) == -1) {
							ctx.user = await checkSign(ctx.uri, ctx.postData, ctx.getData)
							break
						}
					}
					console.log(`${ctx.request.host} request ${ctx.request.url} interface`)
					if(_router['all'] && _router['all'][uri])
						target = _router['all'][uri]
					else 
						target = _router[_method][uri]
					const result = await target(ctx, '&next')
					if(result == '&next') {
						ctx.index++
						await next()
					}
					else {
						console.log(result)
						if(!result && result !== 0 && result !== false) {
							console.error(`${uri} interface is not return data`)
							ctx.status = 500
							ctx.body = parseError('-1003', `${uri} interface is not return data`)
							return
						}
						//如果要求原样输出则按原样输入
						if(ctx.anyOut) {
							ctx.body = result
							return
						}
						let {code, msg, data, echo} = result
						if(echo) {
							ctx.body = data
						}
						else if(!code && code !== '0') {
							ctx.body = parseData(result)
						}
						else {
							ctx.body = parseData(data)
						}
					}
				}
				catch(err) {
					if(!err || !err.code || !err.msg) {
						console.error('server error:', err)
						err = {
							code: '-1001',
							msg: `server error`
						}
					}
					const {code, msg} = err
					console.error(`${ctx.request.method} ${ctx.request.url}  request failed:`, `[${code}]${msg}`)
					ctx.status = 500
					ctx.body = parseError(code, msg)
					common.writeLog('error', 'router handle data failed', err)
				}
			})
		}
		console.log(`router ${routerName} loaded`)
	}
	catch(err) {
		console.error(err)
		console.error(`router ${routerName} load failed`)
		//写入注册路由件失败log
		common.writeLog('error', `router ${handleName} load failed`)
	}
})
//注册路径前缀
router.prefix(config.frame.uriPrefix)
app.use(router.routes()).use(router.allowedMethods())

/**
 *
 * 路由未命中处理
 *
 * 返回不支持请求信息并显示
 * [return] -1000 不支持的请求信息
 *
 */

app.use((ctx, next) => {
	//过滤谷歌浏览器的烦人图标请求
	if(ctx.request.url == '/favicon.ico') {
		ctx.status = 404
		ctx.body = parseError('-1002', 'favicon is not support', true)
		return
	}
	console.error(`${ctx.request.method} ${ctx.request.url} is not support request`)
	const ip =  ctx.headers['x-real-ip'] || ctx.headers['x-forwarded-for'] || (ctx.ip ? ctx.ip.substr(7, ctx.ip.length - 1) : '0.0.0.0')
	console.log('request IP: %s', ip)
	ctx.status = 405
	ctx.body = parseError('-1000', `${ctx.request.method} ${ctx.request.url} is not support request`)
})

/**
 *
 * 框架错误处理
 *
 * 发生意外错误时输出错误
 *
 */

app.on('error', (err, ctx) => {
	console.error('server error', err, ctx)
	//写入服务器错误log
	common.writeLog('error', `server error ${err}`)
});

/**
 *
 * 服务器端口监听处理
 *
 * 正式启动框架并显示
 *
 */

app.listen(serverPort, async () => {
	common.borderMsg('RHRM Frame is running ^-^', `Port: ${serverPort}`)
	//写入服务器启动完毕log
	common.writeLog('info', `server started, port is ${serverPort}`)
})